/** Copyright (C) 2008 Happy Fish / YuQing
 * FastDFS Java Client may be copied only under the terms of the GNU Lesser
 * General Public License (LGPL).
 * Please visit the FastDFS Home Page http://www.csource.org/ for more detail. */

package org.csource.fastdfs;

import java.io.IOException;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.File;
import java.util.Arrays;

import org.csource.common.Base64;
import org.csource.common.MyException;
import org.csource.common.NameValuePair;

import java.net.Socket;

/**
* Storage client for 2 fields file id: group name and filename
* @author Happy Fish / YuQing
* @version Version 1.24
*/
public class StorageClient {
    /**
    * Upload file by file buff
    * @author Happy Fish / YuQing
    * @version Version 1.12
    */
    public static class UploadBuff implements UploadCallback {
        private byte[] fileBuff;
        private int offset;
        private int length;

        /** 
        * constructor
        * @param fileBuff the file buff for uploading
         * @param offset
         * @param length
         */
        public UploadBuff(byte[] fileBuff, int offset, int length) {
            super();
            this.fileBuff = fileBuff;
            this.offset = offset;
            this.length = length;
        }

        /**
        * send file content callback function, be called only once when the file uploaded
        * @param out output stream for writing file content
        * @return 0 success, return none zero(errno) if fail
        */
        public int send(OutputStream out) throws IOException {
            out.write(this.fileBuff, this.offset, this.length);

            return 0;
        }
    }

    public final static Base64 base64 = new Base64('-', '_', '.', 0);
    protected TrackerServer trackerServer;
    protected StorageServer storageServer;
    protected byte errno;

    /**
    * constructor using global settings in class ClientGlobal
    */
    public StorageClient() {
        this.trackerServer = null;
        this.storageServer = null;
    }

    /**
    * constructor with tracker server and storage server
    * @param trackerServer the tracker server, can be null
    * @param storageServer the storage server, can be null
    */
    public StorageClient(TrackerServer trackerServer, StorageServer storageServer) {
        this.trackerServer = trackerServer;
        this.storageServer = storageServer;
    }

    /**
    * get the error code of last call
    * @return the error code of last call
    */
    public byte getErrorCode() {
        return this.errno;
    }

    /**
    * upload file to storage server (by file name)
    * @param local_filename local filename to upload
    * @param file_ext_name file ext name, do not include dot(.), null to extract ext name from the local filename
    * @param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to store the file </li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(String local_filename, String file_ext_name, NameValuePair[] meta_list)
        throws IOException, MyException {
        final String group_name = null;
        return this.upload_file(group_name, local_filename, file_ext_name, meta_list);
    }

    /**
    * upload file to storage server (by file name)
    * @param group_name the group name to upload file to, can be empty
    * @param local_filename local filename to upload
    * @param file_ext_name file ext name, do not include dot(.), null to extract ext name from the local filename
    * @param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to store the file </li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    protected String[] upload_file(String group_name, String local_filename, String file_ext_name,
        NameValuePair[] meta_list) throws IOException, MyException {
        final byte cmd = ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_FILE;
        return this.upload_file(cmd, group_name, local_filename, file_ext_name, meta_list);
    }

    /**
    * upload file to storage server (by file name)
    * @param cmd the command
    * @param group_name the group name to upload file to, can be empty
    * @param local_filename local filename to upload
    * @param file_ext_name file ext name, do not include dot(.), null to extract ext name from the local filename
    * @param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to store the file </li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    protected String[] upload_file(byte cmd, String group_name, String local_filename, String file_ext_name,
        NameValuePair[] meta_list) throws IOException, MyException {
        File f = new File(local_filename);
        FileInputStream fis = new FileInputStream(f);

        if (file_ext_name == null) {
            int nPos = local_filename.lastIndexOf('.');
            if (nPos > 0 && local_filename.length() - nPos <= ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN + 1) {
                file_ext_name = local_filename.substring(nPos + 1);
            }
        }

        try {
            return this.do_upload_file(cmd, group_name, null, null, file_ext_name, f.length(),
                new UploadStream(fis, f.length()), meta_list);
        } finally {
            fis.close();
        }
    }

    /**
    * upload file to storage server (by file buff)
    * @param file_buff file content/buff
    * @param offset start offset of the buff
    * @param length the length of buff to upload
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to store the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(byte[] file_buff, int offset, int length, String file_ext_name,
        NameValuePair[] meta_list) throws IOException, MyException {
        final String group_name = null;
        return this.upload_file(group_name, file_buff, offset, length, file_ext_name, meta_list);
    }

    /**
    * upload file to storage server (by file buff)
    * @param group_name the group name to upload file to, can be empty
    * @param file_buff file content/buff
    * @param offset start offset of the buff
    * @param length the length of buff to upload
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to store the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(String group_name, byte[] file_buff, int offset, int length, String file_ext_name,
        NameValuePair[] meta_list) throws IOException, MyException {
        return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_FILE, group_name, null, null, file_ext_name,
            length, new UploadBuff(file_buff, offset, length), meta_list);
    }

    /**
    * upload file to storage server (by file buff)
    * @param file_buff file content/buff
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to store the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(byte[] file_buff, String file_ext_name, NameValuePair[] meta_list)
        throws IOException, MyException {
        final String group_name = null;
        return this.upload_file(group_name, file_buff, 0, file_buff.length, file_ext_name, meta_list);
    }

    /**
    * upload file to storage server (by file buff)
    * @param group_name the group name to upload file to, can be empty
    * @param file_buff file content/buff
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to store the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(String group_name, byte[] file_buff, String file_ext_name, NameValuePair[] meta_list)
        throws IOException, MyException {
        return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_FILE, group_name, null, null, file_ext_name,
            file_buff.length, new UploadBuff(file_buff, 0, file_buff.length), meta_list);
    }

    /**
    * upload file to storage server (by callback)
    * @param group_name the group name to upload file to, can be empty
    * @param file_size the file size
    * @param callback the write data callback object
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(String group_name, long file_size, UploadCallback callback, String file_ext_name,
        NameValuePair[] meta_list) throws IOException, MyException {
        final String master_filename = null;
        final String prefix_name = null;

        return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_FILE, group_name, master_filename, prefix_name,
            file_ext_name, file_size, callback, meta_list);
    }

    /**
    * upload file to storage server (by file name, slave file mode)
    * @param group_name the group name of master file
    * @param master_filename the master file name to generate the slave file
    * @param prefix_name the prefix name to generate the slave file
    * @param local_filename local filename to upload
    * @param file_ext_name file ext name, do not include dot(.), null to extract ext name from the local filename
    * @param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file </li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(String group_name, String master_filename, String prefix_name, String local_filename,
        String file_ext_name, NameValuePair[] meta_list) throws IOException, MyException {
        if ((group_name == null || group_name.length() == 0)
            || (master_filename == null || master_filename.length() == 0) || (prefix_name == null)) {
            throw new MyException("invalid arguement");
        }

        File f = new File(local_filename);
        FileInputStream fis = new FileInputStream(f);

        if (file_ext_name == null) {
            int nPos = local_filename.lastIndexOf('.');
            if (nPos > 0 && local_filename.length() - nPos <= ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN + 1) {
                file_ext_name = local_filename.substring(nPos + 1);
            }
        }

        try {
            return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_SLAVE_FILE, group_name, master_filename,
                prefix_name, file_ext_name, f.length(), new UploadStream(fis, f.length()), meta_list);
        } finally {
            fis.close();
        }
    }

    /**
    * upload file to storage server (by file buff, slave file mode)
    * @param group_name the group name of master file
    * @param master_filename the master file name to generate the slave file
    * @param prefix_name the prefix name to generate the slave file
    * @param file_buff file content/buff
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(String group_name, String master_filename, String prefix_name, byte[] file_buff,
        String file_ext_name, NameValuePair[] meta_list) throws IOException, MyException {
        if ((group_name == null || group_name.length() == 0)
            || (master_filename == null || master_filename.length() == 0) || (prefix_name == null)) {
            throw new MyException("invalid arguement");
        }

        return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_SLAVE_FILE, group_name, master_filename,
            prefix_name, file_ext_name, file_buff.length, new UploadBuff(file_buff, 0, file_buff.length), meta_list);
    }

    /**
    * upload file to storage server (by file buff, slave file mode)
    * @param group_name the group name of master file
    * @param master_filename the master file name to generate the slave file
    * @param prefix_name the prefix name to generate the slave file
    * @param file_buff file content/buff
    * @param offset start offset of the buff
    * @param length the length of buff to upload
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(String group_name, String master_filename, String prefix_name, byte[] file_buff,
        int offset, int length, String file_ext_name, NameValuePair[] meta_list) throws IOException, MyException {
        if ((group_name == null || group_name.length() == 0)
            || (master_filename == null || master_filename.length() == 0) || (prefix_name == null)) {
            throw new MyException("invalid arguement");
        }

        return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_SLAVE_FILE, group_name, master_filename,
            prefix_name, file_ext_name, length, new UploadBuff(file_buff, offset, length), meta_list);
    }

    /**
    * upload file to storage server (by callback, slave file mode)
    * @param group_name the group name to upload file to, can be empty
    * @param master_filename the master file name to generate the slave file
    * @param prefix_name the prefix name to generate the slave file
    * @param file_size the file size
    * @param callback the write data callback object
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_file(String group_name, String master_filename, String prefix_name, long file_size,
        UploadCallback callback, String file_ext_name, NameValuePair[] meta_list) throws IOException, MyException {
        return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_SLAVE_FILE, group_name, master_filename,
            prefix_name, file_ext_name, file_size, callback, meta_list);
    }

    /**
    * upload appender file to storage server (by file name)
    * @param local_filename local filename to upload
    * @param file_ext_name file ext name, do not include dot(.), null to extract ext name from the local filename
    * @param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file </li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_appender_file(String local_filename, String file_ext_name, NameValuePair[] meta_list)
        throws IOException, MyException {
        final String group_name = null;
        return this.upload_appender_file(group_name, local_filename, file_ext_name, meta_list);
    }

    /**
    * upload appender file to storage server (by file name)
    * @param group_name the group name to upload file to, can be empty
    * @param local_filename local filename to upload
    * @param file_ext_name file ext name, do not include dot(.), null to extract ext name from the local filename
    * @param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file </li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    protected String[] upload_appender_file(String group_name, String local_filename, String file_ext_name,
        NameValuePair[] meta_list) throws IOException, MyException {
        final byte cmd = ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_APPENDER_FILE;
        return this.upload_file(cmd, group_name, local_filename, file_ext_name, meta_list);
    }

    /**
    * upload appender file to storage server (by file buff)
    * @param file_buff file content/buff
    * @param offset start offset of the buff
    * @param length the length of buff to upload
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_appender_file(byte[] file_buff, int offset, int length, String file_ext_name,
        NameValuePair[] meta_list) throws IOException, MyException {
        final String group_name = null;
        return this.upload_appender_file(group_name, file_buff, offset, length, file_ext_name, meta_list);
    }

    /**
    * upload appender file to storage server (by file buff)
    * @param group_name the group name to upload file to, can be empty
    * @param file_buff file content/buff
    * @param offset start offset of the buff
    * @param length the length of buff to upload
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_appender_file(String group_name, byte[] file_buff, int offset, int length,
        String file_ext_name, NameValuePair[] meta_list) throws IOException, MyException {
        return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_APPENDER_FILE, group_name, null, null,
            file_ext_name, length, new UploadBuff(file_buff, offset, length), meta_list);
    }

    /**
    * upload appender file to storage server (by file buff)
    * @param file_buff file content/buff
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_appender_file(byte[] file_buff, String file_ext_name, NameValuePair[] meta_list)
        throws IOException, MyException {
        final String group_name = null;
        return this.upload_appender_file(group_name, file_buff, 0, file_buff.length, file_ext_name, meta_list);
    }

    /**
    * upload appender file to storage server (by file buff)
    * @param group_name the group name to upload file to, can be empty
    * @param file_buff file content/buff
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_appender_file(String group_name, byte[] file_buff, String file_ext_name,
        NameValuePair[] meta_list) throws IOException, MyException {
        return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_APPENDER_FILE, group_name, null, null,
            file_ext_name, file_buff.length, new UploadBuff(file_buff, 0, file_buff.length), meta_list);
    }

    /**
    * upload appender file to storage server (by callback)
    * @param group_name the group name to upload file to, can be empty
    * @param file_size the file size
    * @param callback the write data callback object
    * @param file_ext_name file ext name, do not include dot(.)
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *           <ul><li>results[0]: the group name to berth the file</li></ul>
    *           <ul><li>results[1]: the new created filename</li></ul>
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public String[] upload_appender_file(String group_name, long file_size, UploadCallback callback,
        String file_ext_name, NameValuePair[] meta_list) throws IOException, MyException {
        final String master_filename = null;
        final String prefix_name = null;

        return this.do_upload_file(ProtoCommon.STORAGE_PROTO_CMD_UPLOAD_APPENDER_FILE, group_name, master_filename,
            prefix_name, file_ext_name, file_size, callback, meta_list);
    }

    /**
    * append file to storage server (by file name)
    * @param group_name the group name of appender file
    * @param appender_filename the appender filename
    * @param local_filename local filename to append
    * @return 0 for success, != 0 for error (error no)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int append_file(String group_name, String appender_filename, String local_filename)
        throws IOException, MyException {
        File f = new File(local_filename);
        FileInputStream fis = new FileInputStream(f);

        try {
            return this.do_append_file(group_name, appender_filename, f.length(), new UploadStream(fis, f.length()));
        } finally {
            fis.close();
        }
    }

    /**
    * append file to storage server (by file buff)
    * @param group_name the group name of appender file
    * @param appender_filename the appender filename
    * @param file_buff file content/buff
    * @return 0 for success, != 0 for error (error no)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int append_file(String group_name, String appender_filename, byte[] file_buff)
        throws IOException, MyException {
        return this.do_append_file(group_name, appender_filename, file_buff.length,
            new UploadBuff(file_buff, 0, file_buff.length));
    }

    /**
    * append file to storage server (by file buff)
    * @param group_name the group name of appender file
    * @param appender_filename the appender filename
    * @param file_buff file content/buff
    * @param offset start offset of the buff
    * @param length the length of buff to append
    * @return 0 for success, != 0 for error (error no)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int append_file(String group_name, String appender_filename, byte[] file_buff, int offset, int length)
        throws IOException, MyException {
        return this.do_append_file(group_name, appender_filename, length, new UploadBuff(file_buff, offset, length));
    }

    /**
    * append file to storage server (by callback)
    * @param group_name the group name to append file to
    * @param appender_filename the appender filename
    * @param file_size the file size
    * @param callback the write data callback object
    * @return 0 for success, != 0 for error (error no)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int append_file(String group_name, String appender_filename, long file_size, UploadCallback callback)
        throws IOException, MyException {
        return this.do_append_file(group_name, appender_filename, file_size, callback);
    }

    /**
    * modify appender file to storage server (by file name)
    * @param group_name the group name of appender file
    * @param appender_filename the appender filename
    * @param file_offset the offset of appender file
    * @param local_filename local filename to append
    * @return 0 for success, != 0 for error (error no)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int modify_file(String group_name, String appender_filename, long file_offset, String local_filename)
        throws IOException, MyException {
        File f = new File(local_filename);
        FileInputStream fis = new FileInputStream(f);

        try {
            return this.do_modify_file(group_name, appender_filename, file_offset, f.length(),
                new UploadStream(fis, f.length()));
        } finally {
            fis.close();
        }
    }

    /**
    * modify appender file to storage server (by file buff)
    * @param group_name the group name of appender file
    * @param appender_filename the appender filename
    * @param file_offset the offset of appender file
    * @param file_buff file content/buff
    * @return 0 for success, != 0 for error (error no)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int modify_file(String group_name, String appender_filename, long file_offset, byte[] file_buff)
        throws IOException, MyException {
        return this.do_modify_file(group_name, appender_filename, file_offset, file_buff.length,
            new UploadBuff(file_buff, 0, file_buff.length));
    }

    /**
    * modify appender file to storage server (by file buff)
    * @param group_name the group name of appender file
    * @param appender_filename the appender filename
    * @param file_offset the offset of appender file
    * @param file_buff file content/buff
    * @param buffer_offset start offset of the buff
    * @param buffer_length the length of buff to modify
    * @return 0 for success, != 0 for error (error no)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int modify_file(String group_name, String appender_filename, long file_offset, byte[] file_buff,
        int buffer_offset, int buffer_length) throws IOException, MyException {
        return this.do_modify_file(group_name, appender_filename, file_offset, buffer_length,
            new UploadBuff(file_buff, buffer_offset, buffer_length));
    }

    /**
    * modify appender file to storage server (by callback)
    * @param group_name the group name to modify file to
    * @param appender_filename the appender filename
    * @param file_offset the offset of appender file
    * @param modify_size the modify size
    * @param callback the write data callback object
    * @return 0 for success, != 0 for error (error no)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int modify_file(String group_name, String appender_filename, long file_offset, long modify_size,
        UploadCallback callback) throws IOException, MyException {
        return this.do_modify_file(group_name, appender_filename, file_offset, modify_size, callback);
    }

    /**
    * upload file to storage server
    * @param cmd the command code
    * @param group_name the group name to upload file to, can be empty
    * @param master_filename the master file name to generate the slave file
    * @param prefix_name the prefix name to generate the slave file
    * @param file_ext_name file ext name, do not include dot(.)
    * @param file_size the file size
    * @param callback the write data callback object
    *	@param meta_list meta info array
    * @return  2 elements string array if success:<br>
    *          <ul><li> results[0]: the group name to berth the file</li></ul>
    *          <ul><li> results[1]: the new created filename</li></ul> 
    *         return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    protected String[] do_upload_file(byte cmd, String group_name, String master_filename, String prefix_name,
        String file_ext_name, long file_size, UploadCallback callback, NameValuePair[] meta_list)
        throws IOException, MyException {
        byte[] header;
        byte[] ext_name_bs;
        String new_group_name;
        String remote_filename;
        boolean bNewConnection;
        Socket storageSocket;
        byte[] sizeBytes;
        byte[] hexLenBytes;
        byte[] masterFilenameBytes;
        boolean bUploadSlave;
        int offset;
        long body_len;

        bUploadSlave = ((group_name != null && group_name.length() > 0)
            && (master_filename != null && master_filename.length() > 0) && (prefix_name != null));
        if (bUploadSlave) {
            bNewConnection = this.newUpdatableStorageConnection(group_name, master_filename);
        } else {
            bNewConnection = this.newWritableStorageConnection(group_name);
        }

        try {
            storageSocket = this.storageServer.getSocket();

            ext_name_bs = new byte[ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN];
            Arrays.fill(ext_name_bs, (byte)0);
            if (file_ext_name != null && file_ext_name.length() > 0) {
                byte[] bs = file_ext_name.getBytes(ClientGlobal.g_charset);
                int ext_name_len = bs.length;
                if (ext_name_len > ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN) {
                    ext_name_len = ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN;
                }
                System.arraycopy(bs, 0, ext_name_bs, 0, ext_name_len);
            }

            if (bUploadSlave) {
                masterFilenameBytes = master_filename.getBytes(ClientGlobal.g_charset);

                sizeBytes = new byte[2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE];
                body_len = sizeBytes.length + ProtoCommon.FDFS_FILE_PREFIX_MAX_LEN
                    + ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN + masterFilenameBytes.length + file_size;

                hexLenBytes = ProtoCommon.long2buff(master_filename.length());
                System.arraycopy(hexLenBytes, 0, sizeBytes, 0, hexLenBytes.length);
                offset = hexLenBytes.length;
            } else {
                masterFilenameBytes = null;
                sizeBytes = new byte[1 + 1 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE];
                body_len = sizeBytes.length + ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN + file_size;

                sizeBytes[0] = (byte)this.storageServer.getStorePathIndex();
                offset = 1;
            }

            hexLenBytes = ProtoCommon.long2buff(file_size);
            System.arraycopy(hexLenBytes, 0, sizeBytes, offset, hexLenBytes.length);

            OutputStream out = storageSocket.getOutputStream();
            header = ProtoCommon.packHeader(cmd, body_len, (byte)0);
            byte[] wholePkg = new byte[(int)(header.length + body_len - file_size)];
            System.arraycopy(header, 0, wholePkg, 0, header.length);
            System.arraycopy(sizeBytes, 0, wholePkg, header.length, sizeBytes.length);
            offset = header.length + sizeBytes.length;
            if (bUploadSlave) {
                byte[] prefix_name_bs = new byte[ProtoCommon.FDFS_FILE_PREFIX_MAX_LEN];
                byte[] bs = prefix_name.getBytes(ClientGlobal.g_charset);
                int prefix_name_len = bs.length;
                Arrays.fill(prefix_name_bs, (byte)0);
                if (prefix_name_len > ProtoCommon.FDFS_FILE_PREFIX_MAX_LEN) {
                    prefix_name_len = ProtoCommon.FDFS_FILE_PREFIX_MAX_LEN;
                }
                if (prefix_name_len > 0) {
                    System.arraycopy(bs, 0, prefix_name_bs, 0, prefix_name_len);
                }

                System.arraycopy(prefix_name_bs, 0, wholePkg, offset, prefix_name_bs.length);
                offset += prefix_name_bs.length;
            }

            System.arraycopy(ext_name_bs, 0, wholePkg, offset, ext_name_bs.length);
            offset += ext_name_bs.length;

            if (bUploadSlave) {
                if (masterFilenameBytes != null) {
                    System.arraycopy(masterFilenameBytes, 0, wholePkg, offset, masterFilenameBytes.length);
                    offset += masterFilenameBytes.length;
                }
            }

            out.write(wholePkg);

            if ((this.errno = (byte)callback.send(out)) != 0) {
                return null;
            }

            ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, -1);
            this.errno = pkgInfo.errno;
            if (pkgInfo.errno != 0) {
                return null;
            }

            if (pkgInfo.body.length <= ProtoCommon.FDFS_GROUP_NAME_MAX_LEN) {
                throw new MyException(
                    "body length: " + pkgInfo.body.length + " <= " + ProtoCommon.FDFS_GROUP_NAME_MAX_LEN);
            }

            new_group_name = new String(pkgInfo.body, 0, ProtoCommon.FDFS_GROUP_NAME_MAX_LEN).trim();
            remote_filename = new String(pkgInfo.body, ProtoCommon.FDFS_GROUP_NAME_MAX_LEN,
                pkgInfo.body.length - ProtoCommon.FDFS_GROUP_NAME_MAX_LEN);
            String[] results = new String[2];
            results[0] = new_group_name;
            results[1] = remote_filename;

            if (meta_list == null || meta_list.length == 0) {
                return results;
            }

            int result = 0;
            try {
                result = this.set_metadata(new_group_name, remote_filename, meta_list,
                    ProtoCommon.STORAGE_SET_METADATA_FLAG_OVERWRITE);
            } catch (IOException ex) {
                result = 5;
                throw ex;
            } finally {
                if (result != 0) {
                    this.errno = (byte)result;
                    this.delete_file(new_group_name, remote_filename);
                    return null;
                }
            }

            return results;
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * append file to storage server
    * @param group_name the group name of appender file
    * @param appender_filename the appender filename
    * @param file_size the file size
    * @param callback the write data callback object
    * @return return true for success, false for fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    protected int do_append_file(String group_name, String appender_filename, long file_size, UploadCallback callback)
        throws IOException, MyException {
        byte[] header;
        boolean bNewConnection;
        Socket storageSocket;
        byte[] hexLenBytes;
        byte[] appenderFilenameBytes;
        int offset;
        long body_len;

        if ((group_name == null || group_name.length() == 0)
            || (appender_filename == null || appender_filename.length() == 0)) {
            this.errno = ProtoCommon.ERR_NO_EINVAL;
            return this.errno;
        }

        bNewConnection = this.newUpdatableStorageConnection(group_name, appender_filename);

        try {
            storageSocket = this.storageServer.getSocket();

            appenderFilenameBytes = appender_filename.getBytes(ClientGlobal.g_charset);
            body_len = 2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + appenderFilenameBytes.length + file_size;

            header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_APPEND_FILE, body_len, (byte)0);
            byte[] wholePkg = new byte[(int)(header.length + body_len - file_size)];
            System.arraycopy(header, 0, wholePkg, 0, header.length);
            offset = header.length;

            hexLenBytes = ProtoCommon.long2buff(appender_filename.length());
            System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
            offset += hexLenBytes.length;

            hexLenBytes = ProtoCommon.long2buff(file_size);
            System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
            offset += hexLenBytes.length;

            OutputStream out = storageSocket.getOutputStream();

            System.arraycopy(appenderFilenameBytes, 0, wholePkg, offset, appenderFilenameBytes.length);
            offset += appenderFilenameBytes.length;

            out.write(wholePkg);
            if ((this.errno = (byte)callback.send(out)) != 0) {
                return this.errno;
            }

            ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);
            this.errno = pkgInfo.errno;
            if (pkgInfo.errno != 0) {
                return this.errno;
            }

            return 0;
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * modify appender file to storage server
    * @param group_name the group name of appender file
    * @param appender_filename the appender filename
    * @param file_offset the offset of appender file
    * @param modify_size the modify size
    * @param callback the write data callback object
    * @return return true for success, false for fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    protected int do_modify_file(String group_name, String appender_filename, long file_offset, long modify_size,
        UploadCallback callback) throws IOException, MyException {
        byte[] header;
        boolean bNewConnection;
        Socket storageSocket;
        byte[] hexLenBytes;
        byte[] appenderFilenameBytes;
        int offset;
        long body_len;

        if ((group_name == null || group_name.length() == 0)
            || (appender_filename == null || appender_filename.length() == 0)) {
            this.errno = ProtoCommon.ERR_NO_EINVAL;
            return this.errno;
        }

        bNewConnection = this.newUpdatableStorageConnection(group_name, appender_filename);

        try {
            storageSocket = this.storageServer.getSocket();

            appenderFilenameBytes = appender_filename.getBytes(ClientGlobal.g_charset);
            body_len = 3 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + appenderFilenameBytes.length + modify_size;

            header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_MODIFY_FILE, body_len, (byte)0);
            byte[] wholePkg = new byte[(int)(header.length + body_len - modify_size)];
            System.arraycopy(header, 0, wholePkg, 0, header.length);
            offset = header.length;

            hexLenBytes = ProtoCommon.long2buff(appender_filename.length());
            System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
            offset += hexLenBytes.length;

            hexLenBytes = ProtoCommon.long2buff(file_offset);
            System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
            offset += hexLenBytes.length;

            hexLenBytes = ProtoCommon.long2buff(modify_size);
            System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
            offset += hexLenBytes.length;

            OutputStream out = storageSocket.getOutputStream();

            System.arraycopy(appenderFilenameBytes, 0, wholePkg, offset, appenderFilenameBytes.length);
            offset += appenderFilenameBytes.length;

            out.write(wholePkg);
            if ((this.errno = (byte)callback.send(out)) != 0) {
                return this.errno;
            }

            ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);
            this.errno = pkgInfo.errno;
            if (pkgInfo.errno != 0) {
                return this.errno;
            }

            return 0;
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * delete file from storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @return 0 for success, none zero for fail (error code)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int delete_file(String group_name, String remote_filename) throws IOException, MyException {
        boolean bNewConnection = this.newUpdatableStorageConnection(group_name, remote_filename);
        Socket storageSocket = this.storageServer.getSocket();

        try {
            this.send_package(ProtoCommon.STORAGE_PROTO_CMD_DELETE_FILE, group_name, remote_filename);
            ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);

            this.errno = pkgInfo.errno;
            return pkgInfo.errno;
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * truncate appender file to size 0 from storage server
    * @param group_name the group name of storage server
    *	@param appender_filename the appender filename
    * @return 0 for success, none zero for fail (error code)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int truncate_file(String group_name, String appender_filename) throws IOException, MyException {
        final long truncated_file_size = 0;
        return this.truncate_file(group_name, appender_filename, truncated_file_size);
    }

    /**
    * truncate appender file from storage server
    * @param group_name the group name of storage server
    *	@param appender_filename the appender filename
    * @param truncated_file_size truncated file size
    * @return 0 for success, none zero for fail (error code)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int truncate_file(String group_name, String appender_filename, long truncated_file_size)
        throws IOException, MyException {
        byte[] header;
        boolean bNewConnection;
        Socket storageSocket;
        byte[] hexLenBytes;
        byte[] appenderFilenameBytes;
        int offset;
        int body_len;

        if ((group_name == null || group_name.length() == 0)
            || (appender_filename == null || appender_filename.length() == 0)) {
            this.errno = ProtoCommon.ERR_NO_EINVAL;
            return this.errno;
        }

        bNewConnection = this.newUpdatableStorageConnection(group_name, appender_filename);

        try {
            storageSocket = this.storageServer.getSocket();

            appenderFilenameBytes = appender_filename.getBytes(ClientGlobal.g_charset);
            body_len = 2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + appenderFilenameBytes.length;

            header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_TRUNCATE_FILE, body_len, (byte)0);
            byte[] wholePkg = new byte[header.length + body_len];
            System.arraycopy(header, 0, wholePkg, 0, header.length);
            offset = header.length;

            hexLenBytes = ProtoCommon.long2buff(appender_filename.length());
            System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
            offset += hexLenBytes.length;

            hexLenBytes = ProtoCommon.long2buff(truncated_file_size);
            System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
            offset += hexLenBytes.length;

            OutputStream out = storageSocket.getOutputStream();

            System.arraycopy(appenderFilenameBytes, 0, wholePkg, offset, appenderFilenameBytes.length);
            offset += appenderFilenameBytes.length;

            out.write(wholePkg);
            ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);
            this.errno = pkgInfo.errno;
            return pkgInfo.errno;
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * download file from storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @return file content/buff, return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public byte[] download_file(String group_name, String remote_filename) throws IOException, MyException {
        final long file_offset = 0;
        final long download_bytes = 0;

        return this.download_file(group_name, remote_filename, file_offset, download_bytes);
    }

    /**
    * download file from storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @param file_offset the start offset of the file
    * @param download_bytes download bytes, 0 for remain bytes from offset
    * @return file content/buff, return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public byte[] download_file(String group_name, String remote_filename, long file_offset, long download_bytes)
        throws IOException, MyException {
        boolean bNewConnection = this.newReadableStorageConnection(group_name, remote_filename);
        Socket storageSocket = this.storageServer.getSocket();

        try {
            ProtoCommon.RecvPackageInfo pkgInfo;

            this.send_download_package(group_name, remote_filename, file_offset, download_bytes);
            pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(), ProtoCommon.STORAGE_PROTO_CMD_RESP, -1);

            this.errno = pkgInfo.errno;
            if (pkgInfo.errno != 0) {
                return null;
            }

            return pkgInfo.body;
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * download file from storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @param local_filename  filename on local
    * @return 0 success, return none zero errno if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int download_file(String group_name, String remote_filename, String local_filename)
        throws IOException, MyException {
        final long file_offset = 0;
        final long download_bytes = 0;
        return this.download_file(group_name, remote_filename, file_offset, download_bytes, local_filename);
    }

    /**
    * download file from storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @param file_offset the start offset of the file
    * @param download_bytes download bytes, 0 for remain bytes from offset
    * @param local_filename  filename on local
    * @return 0 success, return none zero errno if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int download_file(String group_name, String remote_filename, long file_offset, long download_bytes,
        String local_filename) throws IOException, MyException {
        boolean bNewConnection = this.newReadableStorageConnection(group_name, remote_filename);
        Socket storageSocket = this.storageServer.getSocket();
        try {
            ProtoCommon.RecvHeaderInfo header;
            FileOutputStream out = new FileOutputStream(local_filename);
            try {
                this.errno = 0;
                this.send_download_package(group_name, remote_filename, file_offset, download_bytes);

                InputStream in = storageSocket.getInputStream();
                header = ProtoCommon.recvHeader(in, ProtoCommon.STORAGE_PROTO_CMD_RESP, -1);
                this.errno = header.errno;
                if (header.errno != 0) {
                    return header.errno;
                }

                byte[] buff = new byte[256 * 1024];
                long remainBytes = header.body_len;
                int bytes;

                // System.out.println("expect_body_len=" + header.body_len);

                while (remainBytes > 0) {
                    if ((bytes = in.read(buff, 0, remainBytes > buff.length ? buff.length : (int)remainBytes)) < 0) {
                        throw new IOException(
                            "recv package size " + (header.body_len - remainBytes) + " != " + header.body_len);
                    }

                    out.write(buff, 0, bytes);
                    remainBytes -= bytes;

                    // System.out.println("totalBytes=" + (header.body_len - remainBytes));
                }

                return 0;
            } catch (IOException ex) {
                if (this.errno == 0) {
                    this.errno = ProtoCommon.ERR_NO_EIO;
                }

                throw ex;
            } finally {
                out.close();
                if (this.errno != 0) {
                    (new File(local_filename)).delete();
                }
            }
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * download file from storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @param callback call callback.recv() when data arrive
    * @return 0 success, return none zero errno if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int download_file(String group_name, String remote_filename, DownloadCallback callback)
        throws IOException, MyException {
        final long file_offset = 0;
        final long download_bytes = 0;
        return this.download_file(group_name, remote_filename, file_offset, download_bytes, callback);
    }

    /**
    * download file from storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @param file_offset the start offset of the file
    * @param download_bytes download bytes, 0 for remain bytes from offset
    * @param callback call callback.recv() when data arrive
    * @return 0 success, return none zero errno if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int download_file(String group_name, String remote_filename, long file_offset, long download_bytes,
        DownloadCallback callback) throws IOException, MyException {
        int result;
        boolean bNewConnection = this.newReadableStorageConnection(group_name, remote_filename);
        Socket storageSocket = this.storageServer.getSocket();

        try {
            ProtoCommon.RecvHeaderInfo header;
            this.send_download_package(group_name, remote_filename, file_offset, download_bytes);

            InputStream in = storageSocket.getInputStream();
            header = ProtoCommon.recvHeader(in, ProtoCommon.STORAGE_PROTO_CMD_RESP, -1);
            this.errno = header.errno;
            if (header.errno != 0) {
                return header.errno;
            }

            byte[] buff = new byte[2 * 1024];
            long remainBytes = header.body_len;
            int bytes;

            // System.out.println("expect_body_len=" + header.body_len);

            while (remainBytes > 0) {
                if ((bytes = in.read(buff, 0, remainBytes > buff.length ? buff.length : (int)remainBytes)) < 0) {
                    throw new IOException(
                        "recv package size " + (header.body_len - remainBytes) + " != " + header.body_len);
                }

                if ((result = callback.recv(header.body_len, buff, bytes)) != 0) {
                    this.errno = (byte)result;
                    return result;
                }

                remainBytes -= bytes;
                // System.out.println("totalBytes=" + (header.body_len - remainBytes));
            }

            return 0;
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * get all metadata items from storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @return meta info array, return null if fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public NameValuePair[] get_metadata(String group_name, String remote_filename) throws IOException, MyException {
        boolean bNewConnection = this.newUpdatableStorageConnection(group_name, remote_filename);
        Socket storageSocket = this.storageServer.getSocket();

        try {
            ProtoCommon.RecvPackageInfo pkgInfo;

            this.send_package(ProtoCommon.STORAGE_PROTO_CMD_GET_METADATA, group_name, remote_filename);
            pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(), ProtoCommon.STORAGE_PROTO_CMD_RESP, -1);

            this.errno = pkgInfo.errno;
            if (pkgInfo.errno != 0) {
                return null;
            }

            return ProtoCommon.split_metadata(new String(pkgInfo.body, ClientGlobal.g_charset));
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * set metadata items to storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    *	@param meta_list meta item array
    * @param op_flag flag, can be one of following values: <br>
    *            <ul><li> ProtoCommon.STORAGE_SET_METADATA_FLAG_OVERWRITE: overwrite all old
    *				       metadata items</li></ul>
    *            <ul><li> ProtoCommon.STORAGE_SET_METADATA_FLAG_MERGE: merge, insert when
    *				       the metadata item not exist, otherwise update it</li></ul>
    * @return 0 for success, !=0 fail (error code)
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public int set_metadata(String group_name, String remote_filename, NameValuePair[] meta_list, byte op_flag)
        throws IOException, MyException {
        boolean bNewConnection = this.newUpdatableStorageConnection(group_name, remote_filename);
        Socket storageSocket = this.storageServer.getSocket();

        try {
            byte[] header;
            byte[] groupBytes;
            byte[] filenameBytes;
            byte[] meta_buff;
            byte[] bs;
            int groupLen;
            byte[] sizeBytes;
            ProtoCommon.RecvPackageInfo pkgInfo;

            if (meta_list == null) {
                meta_buff = new byte[0];
            } else {
                meta_buff = ProtoCommon.pack_metadata(meta_list).getBytes(ClientGlobal.g_charset);
            }

            filenameBytes = remote_filename.getBytes(ClientGlobal.g_charset);
            sizeBytes = new byte[2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE];
            Arrays.fill(sizeBytes, (byte)0);

            bs = ProtoCommon.long2buff(filenameBytes.length);
            System.arraycopy(bs, 0, sizeBytes, 0, bs.length);
            bs = ProtoCommon.long2buff(meta_buff.length);
            System.arraycopy(bs, 0, sizeBytes, ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE, bs.length);

            groupBytes = new byte[ProtoCommon.FDFS_GROUP_NAME_MAX_LEN];
            bs = group_name.getBytes(ClientGlobal.g_charset);

            Arrays.fill(groupBytes, (byte)0);
            if (bs.length <= groupBytes.length) {
                groupLen = bs.length;
            } else {
                groupLen = groupBytes.length;
            }
            System.arraycopy(bs, 0, groupBytes, 0, groupLen);

            header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_SET_METADATA,
                2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + 1 + groupBytes.length + filenameBytes.length
                    + meta_buff.length,
                (byte)0);
            OutputStream out = storageSocket.getOutputStream();
            byte[] wholePkg = new byte[header.length + sizeBytes.length + 1 + groupBytes.length + filenameBytes.length];
            System.arraycopy(header, 0, wholePkg, 0, header.length);
            System.arraycopy(sizeBytes, 0, wholePkg, header.length, sizeBytes.length);
            wholePkg[header.length + sizeBytes.length] = op_flag;
            System.arraycopy(groupBytes, 0, wholePkg, header.length + sizeBytes.length + 1, groupBytes.length);
            System.arraycopy(filenameBytes, 0, wholePkg, header.length + sizeBytes.length + 1 + groupBytes.length,
                filenameBytes.length);
            out.write(wholePkg);
            if (meta_buff.length > 0) {
                out.write(meta_buff);
            }

            pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(), ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);

            this.errno = pkgInfo.errno;
            return pkgInfo.errno;
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * get file info decoded from the filename, fetch from the storage if necessary
    * @param group_name the group name
    *	@param remote_filename the filename
    * @return FileInfo object for success, return null for fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public FileInfo get_file_info(String group_name, String remote_filename) throws IOException, MyException {
        if (remote_filename.length() < ProtoCommon.FDFS_FILE_PATH_LEN + ProtoCommon.FDFS_FILENAME_BASE64_LENGTH
            + ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN + 1) {
            this.errno = ProtoCommon.ERR_NO_EINVAL;
            return null;
        }

        byte[] buff = base64.decodeAuto(remote_filename.substring(ProtoCommon.FDFS_FILE_PATH_LEN,
            ProtoCommon.FDFS_FILE_PATH_LEN + ProtoCommon.FDFS_FILENAME_BASE64_LENGTH));

        long file_size = ProtoCommon.buff2long(buff, 4 * 2);
        if (((remote_filename.length() > ProtoCommon.TRUNK_LOGIC_FILENAME_LENGTH)
            || ((remote_filename.length() > ProtoCommon.NORMAL_LOGIC_FILENAME_LENGTH)
                && ((file_size & ProtoCommon.TRUNK_FILE_MARK_SIZE) == 0)))
            || ((file_size & ProtoCommon.APPENDER_FILE_SIZE) != 0)) { // slave file or appender file
            FileInfo fi = this.query_file_info(group_name, remote_filename);
            if (fi == null) {
                return null;
            }
            return fi;
        }

        FileInfo fileInfo = new FileInfo(file_size, 0, 0, ProtoCommon.getIpAddress(buff, 0));
        fileInfo.setCreateTimestamp(ProtoCommon.buff2int(buff, 4));
        if ((file_size >> 63) != 0) {
            file_size &= 0xFFFFFFFFL; // low 32 bits is file size
            fileInfo.setFileSize(file_size);
        }
        fileInfo.setCrc32(ProtoCommon.buff2int(buff, 4 * 4));

        return fileInfo;
    }

    /**
    * get file info from storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @return FileInfo object for success, return null for fail
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    public FileInfo query_file_info(String group_name, String remote_filename) throws IOException, MyException {
        boolean bNewConnection = this.newUpdatableStorageConnection(group_name, remote_filename);
        Socket storageSocket = this.storageServer.getSocket();

        try {
            byte[] header;
            byte[] groupBytes;
            byte[] filenameBytes;
            byte[] bs;
            int groupLen;
            ProtoCommon.RecvPackageInfo pkgInfo;

            filenameBytes = remote_filename.getBytes(ClientGlobal.g_charset);
            groupBytes = new byte[ProtoCommon.FDFS_GROUP_NAME_MAX_LEN];
            bs = group_name.getBytes(ClientGlobal.g_charset);

            Arrays.fill(groupBytes, (byte)0);
            if (bs.length <= groupBytes.length) {
                groupLen = bs.length;
            } else {
                groupLen = groupBytes.length;
            }
            System.arraycopy(bs, 0, groupBytes, 0, groupLen);

            header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_QUERY_FILE_INFO,
                +groupBytes.length + filenameBytes.length, (byte)0);
            OutputStream out = storageSocket.getOutputStream();
            byte[] wholePkg = new byte[header.length + groupBytes.length + filenameBytes.length];
            System.arraycopy(header, 0, wholePkg, 0, header.length);
            System.arraycopy(groupBytes, 0, wholePkg, header.length, groupBytes.length);
            System.arraycopy(filenameBytes, 0, wholePkg, header.length + groupBytes.length, filenameBytes.length);
            out.write(wholePkg);

            pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(), ProtoCommon.STORAGE_PROTO_CMD_RESP,
                3 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + ProtoCommon.FDFS_IPADDR_SIZE);

            this.errno = pkgInfo.errno;
            if (pkgInfo.errno != 0) {
                return null;
            }

            long file_size = ProtoCommon.buff2long(pkgInfo.body, 0);
            int create_timestamp = (int)ProtoCommon.buff2long(pkgInfo.body, ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE);
            int crc32 = (int)ProtoCommon.buff2long(pkgInfo.body, 2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE);
            String source_ip_addr = (new String(pkgInfo.body, 3 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE,
                ProtoCommon.FDFS_IPADDR_SIZE)).trim();
            return new FileInfo(file_size, create_timestamp, crc32, source_ip_addr);
        } catch (IOException ex) {
            if (!bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }

            throw ex;
        } finally {
            if (bNewConnection) {
                try {
                    this.storageServer.close();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    this.storageServer = null;
                }
            }
        }
    }

    /**
    * check storage socket, if null create a new connection
    * @param group_name the group name to upload file to, can be empty
    * @return true if create a new connection
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    protected boolean newWritableStorageConnection(String group_name) throws IOException, MyException {
        if (this.storageServer != null) {
            return false;
        } else {
            TrackerClient tracker = new TrackerClient();
            this.storageServer = tracker.getStoreStorage(this.trackerServer, group_name);
            if (this.storageServer == null) {
                throw new MyException("getStoreStorage fail, errno code: " + tracker.getErrorCode());
            }
            return true;
        }
    }

    /**
    * check storage socket, if null create a new connection
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @return true if create a new connection
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    protected boolean newReadableStorageConnection(String group_name, String remote_filename)
        throws IOException, MyException {
        if (this.storageServer != null) {
            return false;
        } else {
            TrackerClient tracker = new TrackerClient();
            this.storageServer = tracker.getFetchStorage(this.trackerServer, group_name, remote_filename);
            if (this.storageServer == null) {
                throw new MyException("getStoreStorage fail, errno code: " + tracker.getErrorCode());
            }
            return true;
        }
    }

    /**
    * check storage socket, if null create a new connection
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @return true if create a new connection
     * @throws IOException if an error occurred
     * @throws MyException if an error occurred
    */
    protected boolean newUpdatableStorageConnection(String group_name, String remote_filename)
        throws IOException, MyException {
        if (this.storageServer != null) {
            return false;
        } else {
            TrackerClient tracker = new TrackerClient();
            this.storageServer = tracker.getUpdateStorage(this.trackerServer, group_name, remote_filename);
            if (this.storageServer == null) {
                throw new MyException("getStoreStorage fail, errno code: " + tracker.getErrorCode());
            }
            return true;
        }
    }

    /**
    * send package to storage server
    * @param cmd which command to send
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
     * @throws IOException if an error occurred
    */
    protected void send_package(byte cmd, String group_name, String remote_filename) throws IOException {
        byte[] header;
        byte[] groupBytes;
        byte[] filenameBytes;
        byte[] bs;
        int groupLen;

        groupBytes = new byte[ProtoCommon.FDFS_GROUP_NAME_MAX_LEN];
        bs = group_name.getBytes(ClientGlobal.g_charset);
        filenameBytes = remote_filename.getBytes(ClientGlobal.g_charset);

        Arrays.fill(groupBytes, (byte)0);
        if (bs.length <= groupBytes.length) {
            groupLen = bs.length;
        } else {
            groupLen = groupBytes.length;
        }
        System.arraycopy(bs, 0, groupBytes, 0, groupLen);

        header = ProtoCommon.packHeader(cmd, groupBytes.length + filenameBytes.length, (byte)0);
        byte[] wholePkg = new byte[header.length + groupBytes.length + filenameBytes.length];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        System.arraycopy(groupBytes, 0, wholePkg, header.length, groupBytes.length);
        System.arraycopy(filenameBytes, 0, wholePkg, header.length + groupBytes.length, filenameBytes.length);
        this.storageServer.getSocket().getOutputStream().write(wholePkg);
    }

    /**
    * send package to storage server
    * @param group_name the group name of storage server
    *	@param remote_filename filename on storage server
    * @param file_offset the start offset of the file
    * @param download_bytes download bytes
     * @throws IOException if an error occurred
    */
    protected void send_download_package(String group_name, String remote_filename, long file_offset,
        long download_bytes) throws IOException {
        byte[] header;
        byte[] bsOffset;
        byte[] bsDownBytes;
        byte[] groupBytes;
        byte[] filenameBytes;
        byte[] bs;
        int groupLen;

        bsOffset = ProtoCommon.long2buff(file_offset);
        bsDownBytes = ProtoCommon.long2buff(download_bytes);
        groupBytes = new byte[ProtoCommon.FDFS_GROUP_NAME_MAX_LEN];
        bs = group_name.getBytes(ClientGlobal.g_charset);
        filenameBytes = remote_filename.getBytes(ClientGlobal.g_charset);

        Arrays.fill(groupBytes, (byte)0);
        if (bs.length <= groupBytes.length) {
            groupLen = bs.length;
        } else {
            groupLen = groupBytes.length;
        }
        System.arraycopy(bs, 0, groupBytes, 0, groupLen);

        header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_DOWNLOAD_FILE,
            bsOffset.length + bsDownBytes.length + groupBytes.length + filenameBytes.length, (byte)0);
        byte[] wholePkg = new byte[header.length + bsOffset.length + bsDownBytes.length + groupBytes.length
            + filenameBytes.length];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        System.arraycopy(bsOffset, 0, wholePkg, header.length, bsOffset.length);
        System.arraycopy(bsDownBytes, 0, wholePkg, header.length + bsOffset.length, bsDownBytes.length);
        System.arraycopy(groupBytes, 0, wholePkg, header.length + bsOffset.length + bsDownBytes.length,
            groupBytes.length);
        System.arraycopy(filenameBytes, 0, wholePkg,
            header.length + bsOffset.length + bsDownBytes.length + groupBytes.length, filenameBytes.length);
        this.storageServer.getSocket().getOutputStream().write(wholePkg);
    }
}
